package biback.utils

import enumeratum.ContextUtils
import enumeratum.ContextUtils.Context

import scala.collection.immutable.{IndexedSeq, List, Seq}
import scala.util.control.NonFatal

object AggregateMacros {
  def findCommandsImpl[A: c.WeakTypeTag](c: Context): c.Expr[IndexedSeq[A]] = {
    import c.universe._
    val typeSymbol = weakTypeOf[A].typeSymbol
    validateType(c)(typeSymbol)
    val subclassSymbols = enclosedSubClasses(c)(typeSymbol)
    buildSeqExpr[A](c)(subclassSymbols)
  }

  private def validateType(c: Context)(typeSymbol: c.universe.Symbol): Unit = {
    if (!typeSymbol.asClass.isSealed)
      c.abort(
        c.enclosingPosition,
        "You can only use findCommands on sealed traits or classes"
      )
  }

  /**
    * Finds the actual trees in the current scope that implement objects of the given type
    *
    * aborts compilation if:
    *
    * - the implementations are not all objects
    * - the current scope is not an object
    */
  private def enclosedSubClassTrees(c: Context)(
    typeSymbol: c.universe.Symbol
  ): Seq[c.universe.ClassDef] = {
    import c.universe._
    val enclosingBodySubClassTrees: List[Tree] = try {
      val enclosingModule = c.enclosingClass match {
        case md @ ModuleDef(_, _, _) => md
        case _ =>
          c.abort(
            c.enclosingPosition,
            "The enum (i.e. the class containing the case objects and the call to `findValues`) must be an object"
          )
      }
      enclosingModule.impl.body.filter { x =>
        try {
          x.symbol.isClass &&
            x.symbol.asClass.baseClasses.contains(typeSymbol)
        } catch {
          case NonFatal(e) =>
            c.warning(
              c.enclosingPosition,
              s"Got an exception, indicating a possible bug in Enumeratum. Message: ${e.getMessage}"
            )
            false
        }
      }
    } catch {
      case NonFatal(e) =>
        c.abort(c.enclosingPosition, s"Unexpected error: ${e.getMessage}")
    }
    c.echo(c.enclosingPosition, s"${enclosingBodySubClassTrees.length}")
    if (isDocCompiler(c))
      enclosingBodySubClassTrees.flatMap {
        case docDef if isDocDef(c)(docDef) => {
          docDef.children.collect {
            case m: ClassDef => m
          }
        }
        case moduleDef: ClassDef => List(moduleDef)
      }
    else
      enclosingBodySubClassTrees.collect {
        case m: ClassDef => m
      }
  }

  /**
    * Returns a sequence of symbols for objects that implement the given type
    */
  private def enclosedSubClasses(c: Context)(
    typeSymbol: c.universe.Symbol
  ): Seq[c.universe.Symbol] = {
    enclosedSubClassTrees(c)(typeSymbol).map(_.symbol)
  }

  /**
    * Builds and returns an expression for an IndexedSeq containing the given symbols
    */
  @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
  private def buildSeqExpr[A: c.WeakTypeTag](c: Context)(
    subclassSymbols: Seq[c.universe.Symbol]
  ) = {
    import c.universe._
    val resultType = weakTypeOf[A]
    if (subclassSymbols.isEmpty) {
      c.Expr[IndexedSeq[A]](reify(IndexedSeq.empty[A]).tree)
    } else {
      c.Expr[IndexedSeq[A]](
        Apply(
          TypeApply(
            Select(reify(IndexedSeq).tree, ContextUtils.termName(c)("apply")),
            List(TypeTree(resultType))
          ),
          subclassSymbols.map(Ident(_)).toList
        )
      )
    }
  }

  /**
    * Returns whether or not we are in doc mode.
    *
    * It's a bit of a hack, but I don't think it's much worse than pulling in scala-compiler
    * for the sake of getting access to this class and doing an `isInstanceOf`
    */
  private[this] def isDocCompiler(c: Context): Boolean = {
    c.universe.getClass.toString.contains("doc.DocFactory")
  }

  /**
    * Returns whether or not a given tree is a DocDef
    *
    * DocDefs are not part of the public API, so we try to hack around it here.
    */
  private[this] def isDocDef(c: Context)(t: c.universe.Tree): Boolean = {
    t.getClass.toString.contains("DocDef")
  }}
